Skip to content

chore: merge current upstream dev into clean Telegram successor - #10

Merged
twoimo merged 12 commits into
successor/telegram-tool-activity-cleanfrom
tmp/upstream-dev-10a32cfd
Jul 25, 2026
Merged

chore: merge current upstream dev into clean Telegram successor#10
twoimo merged 12 commits into
successor/telegram-tool-activity-cleanfrom
tmp/upstream-dev-10a32cfd

Conversation

@twoimo

@twoimo twoimo commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Internal synchronization PR. This merges current upstream dev into the preserved Telegram successor without squashing either history.

Yeachan-Heo and others added 12 commits July 25, 2026 11:03
…Yeachan-Heo#3109)

* feat(agents): give read-only role agents irc and read-only git access

`architect`, `planner`, and `critic` pin an explicit `tools:` allow-list, so
they only receive the tools they name. That excluded `irc`, and their
`bashAllowedPrefixes` allowed only `gjc ralplan --write` / `gjc state`, so
every git command was blocked.

- add `irc` to the three agents' tool lists (`executor` and generic subagents
  declare no `tools:` frontmatter and already inherit `irc` from the builtin
  set, so they are intentionally untouched)
- add read-only git prefixes: status, log, show, diff, blame, rev-parse,
  ls-files. These agents use the workflow bash profile, where
  `validateMatchedGjcCommand` passes any non-`gjc` command once a prefix
  matches, so mutating git (commit/push/reset/checkout/branch -D/config) and
  arbitrary shell stay blocked and the read-only contract holds.
- sync AGENTS.md and the shared restricted-bash prompt fragment, which both
  documented the old `gjc`-only contract
- add a behavioral regression test driving the real bundled agent
  definitions through `checkBashAllowedPrefixes`, rather than only pinning
  the prefix array

`web_search` was already present in all three lists; it is now asserted.

* fix(task): keep irc active for subagents instead of hiding it behind discovery

`irc` is `loadMode: "discoverable"`, and `tools.discoveryMode` defaults to
`"all"`, so the initial-tool filter in `createAgentSession` dropped it from
every subagent that does not pin an explicit `tools:` list — notably
`executor`. The tool was constructed and reachable, but only after the model
spent a `search_tool_bm25` round-trip to find it, which is the wrong tradeoff
for a coordination channel the agent needs proactively.

- add `CreateAgentSessionOptions.alwaysActiveToolNames`: discoverable built-ins
  that stay in the initial active set even under `discoveryMode: "all"`
- pass `["irc"]` from the subagent executor when the parent runtime reports
  IRC is actually available (`ircAvailable`), so behavior still follows the
  existing `irc.enabled` + peer-roster gating
- cover it with a session-level regression test asserting irc is hidden by
  default for a subagent-style session, active when forced, and that forcing
  one tool does not drag in the rest of the discoverable set

Read-only role agents are unaffected: they pass explicit tool names, so irc
already survived the filter via the explicitly-requested path.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…h race (Yeachan-Heo#3111)

* test: remediate 11 confirmed-flaky tests and 1 superseded member; fix broker identity publish race

Phase 2 of the flaky-CI stabilization program (audit-scoped, approval-gated):

- sdk/broker/identity.ts: atomic identity publication via same-directory
  temp file + hard link with EEXIST reread (real product race reproduced
  under 4x parallel contention: "Invalid broker identity key")
- sdk-broker.test.ts: new concurrent identity-publication regression test
- 11 flaky-test rewrites fixing adjudicated root causes (deterministic
  awaits over fixed sleeps, fixture isolation, lease/heartbeat seams,
  bounded polling, mkdtemp AuthStorage isolation, scoped timeouts)
- session-directory.test.ts: remove one superseded append-only-sidecar
  member (superseded by retained-cleanup contract, c49c259)

Evidence chain: 62-suspect audit (2-week window, 3 reconciliation
identities, blinded second validation, third-adjudicator protocol),
24 verification receipts, mutation-probe matrix, 220/220 stress +
80/80 contention iterations. No workflow/branch-protection changes;
dev PR CI critical path untouched.

* style: apply biome formatting to Phase 2 remediation test edits

Formatter-only (line wrapping); no logic change. Fixes the
check:@gajae-code/coding-agent CI failure on this branch.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…sh (Yeachan-Heo#3115)

Follow-up to Yeachan-Heo#3109, which gave `architect`/`planner`/`critic` read-only git
but had to document a workaround: `git diff HEAD~1` was rejected and had to be
quoted as `git diff 'HEAD~1'` (or rewritten as `HEAD^` / an explicit SHA).

The restricted-bash parser treated `~` as an unsafe expansion character
anywhere in a command. Bash only performs tilde expansion when the tilde opens
a word, so `HEAD~1` is a literal argument and the rejection was a false
positive against ordinary git revision syntax.

`~` is now checked positionally instead of unconditionally: word-initial
tildes (`~`, `~/path`, `~user`) are still rejected because bash really does
expand them, while a tilde inside a word is allowed. Every other expansion
character (`$`, `*`, `?`, `[`, `]`, `{`, `}`), command substitution, control
operators, and backslash escapes are unchanged.

Verified against real bash: `HEAD~1`, `HEAD~2`, `HEAD~1..HEAD`, `HEAD~5`, and
`main...HEAD` all print literally, while `~`, `~/secrets`, and `~root` expand
to home directories and therefore stay blocked.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…an-Heo#3117) (Yeachan-Heo#3121)

* fix(security): reject bash tilde expansion in assignment words (Yeachan-Heo#3117)

Follow-up repair to Yeachan-Heo#3115, which made the restricted role-agent bash parser
treat `~` positionally but only rejected it at the start of a whitespace-
delimited word. Bash also performs tilde expansion inside an assignment word:
directly after the first `=` and after each `:` in the assigned value, so
`A=~`, `foo=~root/bar`, `A=x:~`, and `a=x:~:y:~` all expanded while the parser
accepted them.

`parseShellWords` now tracks per-token assignment state and rejects an unquoted
`~` at every position bash can expand it: word start, right after the first
unquoted `=` of an assignment word, and right after an unquoted `:` in that
word's value. An assignment word requires the raw characters before the first
unquoted `=` to match `[A-Za-z_][A-Za-z0-9_]*` with no quoting, so
non-assignment tokens (`--opt=~`, `a-b=~`, `1abc=~`, `=~`) and non-expansion
positions (`a=x~y`, `a=x:y~z`, `a=b=~`) stay allowed, as do literal mid-word git
revisions such as `HEAD~1`. Token state resets at whitespace boundaries.

Verified against real bash: `A=~`, `A=x:~` expand to home paths while
`a=b=~`, `a=x~y`, and `HEAD~1` stay literal. Parameter expansion, command
substitution, control operators, newlines, backslash escapes, and the tilde
denial reason string are unchanged.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

* fix(security): recognize compound `+=` assignment words in restricted bash (Yeachan-Heo#3117)

Adversarial review of Yeachan-Heo#3121 found a fail-open bypass: bash(1) defines an
assignment word as both `name=value` and `name+=value`, and both tilde-expand
in the value, but `ASSIGNMENT_NAME_PATTERN` was tested against the region
before the first unquoted `=`, which holds `A+` for `A+=`. `A+` failed the
name pattern, so `tildeExpandable` was never armed and `A+=~`, `A+=x:~`, and
`A+=~root/bar` were allowed through despite real bash expanding all three.

The assignment-name pattern now accepts an optional single trailing `+`, so a
compound assignment word arms tilde expansion on exactly the same positions as
a plain one, including the `:` continuation in the value. Nothing else changes:
`a++=~`, `+=~`, and `a+b=~` still fail name validation and stay allowed, as do
`a+=x~y` and `a+=b=~` where bash does not expand.

Verified against bash 5.1.16 with HOME=/HOMEDIR: `A+=~` and `A+=x:~` expand,
while `a+=x~y`, `a+=b=~`, `a++=~`, `+=~`, and `a+b=~` stay literal. The denial
reason string is unchanged.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…eo#3122)

* ci: add non-required rehearsal dispatch mode to release CI

The stabilization plan requires proving the release graph is green without
waiting for a real tag (tags are non-deterministic and must not gate a soak).
Every ci.yml job is already switched on `startsWith(github.ref, 'refs/tags/v')`,
so a workflow_dispatch rehearsal mode reuses the EXACT production job
definitions in place — no reusable-workflow extraction, no drift risk, and
job IDs / check context names are unchanged.

- `workflow_dispatch` input `rehearsal`: tag-build-verify | main-nontag
- tag-graph jobs (native, binaries) additionally run under
  dispatch:tag-build-verify; non-tag jobs under dispatch:main-nontag
- publish stays tag-only AND is explicitly excluded from dispatch
  (`github.event_name != 'workflow_dispatch'`), so NPM_TOKEN — referenced
  only inside publish, with no `secrets: inherit` anywhere — is unreachable
  from any rehearsal
- concurrency group gains a dispatch-mode suffix so a rehearsal can never
  cancel a real release run

Behavior for push-main, pull_request, and push-tag is unchanged: the added
clauses only constrain workflow_dispatch. Release-policy and publish-order
guard tests updated accordingly (35 tests green).

* test(ci): expect test:@gajae-code/stats in the native-change push plan

packages/stats has a `test` script and a real test/ directory, so the
planner correctly emits test:@gajae-code/stats for full push plans. The
hardcoded expectation in the native-workspace planning test was stale and
fails on clean dev — this PR's workflow edit merely routes the selftest
selector into the plan, which surfaced it.

Verified: fails identically at origin/dev without this change.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…view state (Yeachan-Heo#3127)

Two defects made the deep-interview workflow unusable in practice.

**Guard blocked `/dev/null` redirects.** `extractBashTargets` captured every
`>`/`>>` target, so `cmd 2>/dev/null` was treated as a repository write and
blocked during any planning phase — including the brownfield exploration
deep-interview itself mandates before Round 1.

Only `/dev/null` is exempted. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>`
stay blocked: they are descriptor aliases, and `exec 1<>src/product.ts;
printf x >/dev/stdout` reaches a real file through a rebound descriptor.

Suppressing a sink target must not turn a mutating command into an
empty-target "safe" verdict, so this also closes the bypasses that suppression
would otherwise open:
- `exec` redirections fail closed (descriptor rebinding is not statically resolvable)
- `>|`, `>&path`, and `<>` write forms are recognized
- path-qualified writers (`/bin/dd`, `/usr/bin/tee`) are matched
- every `dd of=` operand is inspected, not just the first (GNU dd honors the last)
- a redirect capture that dequotes to nothing is `unknown`, not safe

**Hook-seeded state failed native validation.** The draft CLI ran
`validateDeepInterviewV1Envelope` whenever `verifyWorkflowEnvelopeReceiptValue`
returned `native-valid`, but that verdict covers receipt shape and checksum
only — it does not imply a native v1 body. The hook's minimal `ModeState` has
no `schema_version` and no `state`, so every typed operation failed with
`DI_STATE_SCHEMA_INVALID` and interviews ran fully manually with `rounds: []`.

Gate on `isNativeDeepInterviewV1` instead, matching the already-correct
`transformGuardedWorkflowEnvelopeAtomic`, and seed a native v1 envelope from
the hook. The seed omits `initial_idea`: pre-seeding `""` made a later
`initialize-context` carrying a real idea fail with `DI_SETUP_CONFLICT`.

Round shells now canonicalize the agent-supplied `deepInterview.dimension`
label, which is free text on post-topology asks but must be a canonical id in
persisted state.

.gjc/** writes remain blocked in every case.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
* Keep ACP eval execution inside the established permission boundary

Eval-triggered JavaScript and Python tool dispatch must pass through the same prepared session tool path as model-issued calls, so ACP policy remains authoritative while recursion and lifecycle ownership stay centralized. This preserves the accepted implementation direction from the closed contribution chain without unrelated repairs.

Constraint: Fresh replacement is based exactly on upstream/dev e3c7f6a.

Constraint: Publication remains gated on terminal-success exact-dev CI plus explicit owner authorization.

Rejected: Add a second eval-specific authorization layer | it would duplicate policy and risk disagreement with the canonical session boundary.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Keep nested eval tool dispatch routed through getToolForExecution; raw registry lookup would bypass ACP session policy.

Tested: Exact six-file ACP/SDK/Python integration set (98 tests, 488 assertions); coding-agent Biome and TypeScript checks; operation inventory generation and --check; native addon build; production binary build; root/source and compiled-binary CLI smokes; isolated compiled-binary and source-linked install smokes; git diff, stable patch-id, and merge-tree checks.

Not-tested: Hosted-only Linux x64 tarball install substep requiring the CI-provided pi_natives.linux-x64 addon.

Related: Yeachan-Heo#2737, Yeachan-Heo#2980, Yeachan-Heo#2992

* docs(changelog): move ACP eval permission entry into Unreleased

The Yeachan-Heo#2737 sweep note required the changelog entry to land in the current
Unreleased/Fixed section. At head 0ae2315 it was appended to the already
released [0.11.8] block instead, retroactively editing a shipped release
section. Moved verbatim into [Unreleased] > Fixed; no other change.

Co-authored-by: Oreochococukie <Oreochococukie@users.noreply.github.com>

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
Co-authored-by: Oreochococukie <Oreochococukie@users.noreply.github.com>
The workflow-level `permissions: contents: write` grant was redundant:
`publish` is the only job that needs write and already declares its own
job-level `contents: write` override. The workflow-level grant handed a
write-scoped GITHUB_TOKEN to every build/verify job, including the new
rehearsal dispatches, which only check out and build.

No job outside `publish` references GITHUB_TOKEN, github.token, `gh`, or
`git push`, so narrowing the default is behavior-neutral for real
push/PR/tag runs while removing unnecessary write scope from rehearsals.

Surfaced by an independent audit of the stabilization program.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…ancellation/timeout regression (successor to Yeachan-Heo#3112) (Yeachan-Heo#3131)

* fix(vendor/insane-search): P0 — 429 rate-limit no longer kills fetch + bias_check violations

Problem 1: RATE_LIMITED was in TERMINAL_NONSUCCESS, causing a single 429
on the probe or any grid candidate to immediately terminate the entire
fetch pipeline — no grid diversity, no browser fallback, no backoff.

Fix:
- Remove RATE_LIMITED from TERMINAL_NONSUCCESS (validators.py)
- Add _rate_limit_backoff() with linear escalation (2s→10s cap) and
  Retry-After header support (fetch_chain.py)
- Grid now skips rate-limited candidates and continues to the next
  TLS/referer combo instead of breaking
- Browser fallback is no longer skipped on 429

Problem 2: bias_check CI gate reported violations for WAF vendor domains
(funcaptcha.com) and Jina Reader (r.jina.ai) used as infrastructure.

Fix:
- Add funcaptcha.com, api.funcaptcha.com, r.jina.ai, jina.ai to
  URL_ALLOWLIST in bias_check.py with explanatory comments

Verified:
- bias_check → clean
- TERMINAL_NONSUCCESS == {auth_required, not_found} (429 excluded)

* fix(vendor/insane-search): repair two NameErrors in SessionPool.warmup + record local patches

engine/transport.py SessionPool.warmup() referenced two names that do not
exist in its scope:

  1. allow_private  — computed inline into classify_url() but never bound,
                      then passed to _fetch_following() on the next line.
  2. DEFAULT_MAX_REDIRECTS — lives in engine.safety; the sibling call site
                      at get_or_create() already qualifies it as
                      safety.DEFAULT_MAX_REDIRECTS.

Every warmup() call that passed the safety gate raised NameError, so the
per-(host, impersonate) root warmup — the step that lets a WAF sensor set
a resolved session cookie before the deep request — never actually ran.
engine/tests/test_u4.py failed on the vendored tree because of this.

Also records all three local fixes in MANIFEST.json localPatches, per the
vendoring convention, so a future re-vendor from upstream does not
silently drop them.

Verified:
- engine/tests: 8/8 files pass (was 7/8)
- engine/bias_check.py: clean
- bun run ci:check:full: exit 0

* fix(vendor/insane-search): pass resp to _rate_limit_backoff on all 429 paths + probe backoff + tests

Review findings addressed (PR Yeachan-Heo#3104):

1. _rate_limit_backoff(resp) now receives the actual response on every 429
   path (grid AND probe), so server-provided Retry-After is read and honoured
   (numeric, capped at 30s). Previously resp was discarded.

2. Probe-phase 429 now triggers backoff before entering the grid, preventing
   the first grid candidate from hitting the same rate window.

3. Deterministic regression tests (engine/tests/test_rate_limit_backoff.py):
   - Retry-After header read + cap at 30s
   - Probe-phase 429 backoff
   - Grid-phase 429 passes resp
   - Repeated 429s bounded by attempt budget, terminate correctly
   - 429 does not defeat Playwright browser fallback
   - Linear escalation (base*1..base*5 cap)
   All sleeps mocked; no real delays.

4. transport.py: SECURITY NOTE clarifies the SSRF guard is resolver-level
   (pre-connect) only — NOT transport-bound DNS validation. Does not claim
   equivalence to DNS rebinding or redirect-to-private authority protection.

5. MANIFEST.json localPatches entry updated to reflect the Retry-After fix
   and test coverage.

* fix(vendor/insane-search): clamp/validate rate-limit backoff base + cancellation/timeout regression

Addresses REQUEST_CHANGES on Yeachan-Heo#3112 (bounded/cancellable backoff contract).

P0: _rl_base = float(INSANE_RATE_LIMIT_BACKOFF_S) was neither validated nor
capped. Verified failure modes: a non-numeric value raised ValueError inside
_fetch_core; NaN produced an undefined time.sleep; inf hung time.sleep forever
(defeating any timeout); a negative value raised ValueError in time.sleep; a
huge value bypassed the advertised 30s bound (the cap only applied to the
Retry-After header, not the base-derived delay).

Fix:
- Add fetch_chain._clamp_rate_limit_base(): a total parser that rejects
  non-numeric/NaN/infinite/negative values (falls back to the 2s default) and
  clamps a valid value to the 30s ceiling (_RATE_LIMIT_MAX_DELAY).
- Hard-cap the final per-attempt delay to _RATE_LIMIT_MAX_DELAY with a
  finite/non-negative guard before time.sleep, so the bound holds end-to-end
  even for a huge-but-finite base, and sleeps stay short enough that an
  abort/KeyboardInterrupt between attempts is honoured promptly (cancellable).

Tests (test_rate_limit_backoff.py):
- The header claimed abort/timeout coverage but contained none. Fixed the
  header and added the missing deterministic regressions:
  * cancellation: KeyboardInterrupt raised during a backoff sleep propagates
    immediately and cuts the chain short (backoff does not swallow the abort);
  * timeout: no single sleep exceeds 30s or becomes non-finite even under a
    huge base AND a huge Retry-After, so a per-attempt deadline can rely on it;
  * base-validation: helper unit test + end-to-end huge/negative/NaN/inf/
    non-numeric env cases.
- All new tests fail/error against the unfixed code and pass with the fix
  (14 tests total, all deterministic, mocked sleep).

Provenance: kept the MANIFEST.json local-patch entry and documented the
hardening accurately.

Boundary: the disabled production TypeScript bridge is intentionally untouched
— DNS authority is not transport-bound, so that boundary stays closed.

Gates: test_rate_limit_backoff.py (14), test_hardening.py (4), bias_check.py
(clean), scripts/verify-insane-vendor.ts (passed).

---------

Co-authored-by: dmae97 <dmae97@users.noreply.github.com>
Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…4 migration before publishing successor identity (Yeachan-Heo#3080)

Gate the successor `'/tmp/gjc-local/019f97df-38e7-7000-a304-bd5a7ea8e374'` root before the successor session identity is published, across `/new` (both lease branches), `fork()`, handoff, `/resume` switchSession, and branch/tree-jump.

Adversarial exact-head review approved at review #4778681080 for head a1878d7.

Verification at merge time:
- Per-site rotation -> gate -> publication ordering confirmed at all six sites.
- Exhaustive adjudication of the full #beginSessionTransition set: clear-context, navigate-tree, and compact perform no identity rotation and correctly need no gate.
- Order-inversion mutation testing proved the added invariant test is non-vacuous (fails with markerExists=false when the gate is moved after identity publication on the covered path).
- Focused suites 13/13 and 66/66 pass, deterministic across 3 runs; coding-agent typecheck clean.
- Exact-head CI green on a1878d7 (15 success, 5 conditional skips, 0 failures).
- Current dev da7cc11 CI terminal success (24 success, 5 skipped, 0 failures).
- merge-tree against current dev: no conflicts; dev's two intervening commits touch disjoint files.

Non-blocking follow-ups noted in review: the pre-existing cleanup_pending sync/async marker asymmetry in internal-urls (untouched by this PR), and order-inversion regression coverage for the five gated sites the new test does not exercise.
* feat(models): repoint opus presets to claude-opus-5

Anthropic shipped claude-opus-5 (2026-07-24) with the same published
envelope as claude-opus-4-8: 1M context, 128k output, effort range
low..max, 5/25 pricing. Repoint the opus-related built-in presets to it.

- claude-opus: default/planner/critic/architect -> anthropic/claude-opus-5
  with existing effort suffixes; executor stays anthropic/claude-sonnet-5
- opus-codex: default -> anthropic/claude-opus-5:xhigh; the durable
  anthropic/claude-sonnet-5 planner override is unchanged
- fable-opus-codex: planner/critic -> anthropic/claude-opus-5; the fable
  default and codex roles are unchanged
- Regenerate packages/ai/src/models.json so anthropic/claude-opus-5
  resolves; zero providers or models removed
- Extend the generator's Claude Opus vision normalization to opus-5,
  which upstream shipped without image input on three variants, and
  replace the duplicated substring guard with a shared exact-generation
  parser plus a tripwire test that fails when the catalog bundles an
  Opus generation newer than any reviewed one
- Realign preset expectation tests and update docs; measured opus-4-8
  figures in docs/multi-vendor-profiles.md stay attributed to opus-4-8
  rather than silently transferring to an unmeasured model

* fix(models): remove unrelated opus preset catalog drift

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@twoimo
twoimo merged commit cff6567 into successor/telegram-tool-activity-clean Jul 25, 2026
48 of 77 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants